<?php

/**
 * 定义 QColl 类
 *
 * @link http://qeephp.com/
 * @copyright Copyright (c) 2006-2009 Qeeyuan Inc. {@link http://www.qeeyuan.com}
 * @license New BSD License {@link http://qeephp.com/license/}
 * @version $Id: coll.php 2547 2009-06-10 09:24:29Z dualface $
 * @package core
 */

/**
 * QColl 实现了一个类型安全的对象集合
 *
 * QColl 会检查每一个对象的类型是否符合预期，以便将同一类型的对象组织在一起。
 * QColl 具有和 PHP 内置数组相似的性质，因此可以按照使用数组的方式来使用 QColl 集合。
 *
 * 在构造一个集合时，必须指定该集合能够容纳的对象类型：
 *
 * @code php
 * $coll = new QColl('MyObject');
 * $coll[] = new MyObject();
 *
 * // 在尝试存入 MyObject2 类型的对象到 $coll 中时将抛出异常
 * $coll[] = new MyObject2();
 *
 * // 指定一个对象
 * $coll[$offset] = $item;
 *
 * // 遍历一个集合
 * foreach ($coll as $offset => $item)
 * {
 *     dump($item, $offset);
 * }
 * @endcode
 *
 * @author YuLei Liao <liaoyulei@qeeyuan.com>
 * @version $Id: coll.php 2547 2009-06-10 09:24:29Z dualface $
 * @package core
 */
class QColl implements Iterator, ArrayAccess, Countable
{
    /**
     * 集合对象的类型
     *
     * @var string
     * @access private
     */
    protected $_type;

    /**
     * 保存对象的数组
     *
     * @var array
     * @access private
     */
    protected $_coll = array();

    /**
     * 指示迭代对象是否有效
     *
     * @var boolean
     * @access private
     */
    protected $_is_valid = false;

    /**
     * 构造函数
     *
     * @param string $type 集合对象类型
     */
    function __construct($type)
    {
        $this->_type = $type;
    }

    /**
     * 从数组创建一个集合
     *
     * QColl::createFromArray() 方法从一个包含多个对象的数组创建集合。
     * 新建的集合包含数组中的所有对象，并且确保对象的类型符合要求。
     *
     * @param array $objects 包含多个对象的数组
     * @param string $type 集合对象类型
     * @param boolean $keep_keys 是否在创建集合时保持数组的键名
     *
     * @return QColl 新创建的集合对象
     */
    static function createFromArray(array $objects, $type, $keep_keys = false)
    {
        $coll = new QColl($type);
        if ($keep_keys)
        {
            foreach ($objects as $offset => $object) $coll[$offset] = $object;
        }
        else
        {
            foreach ($objects as $object) $coll[] = $object;
        }
        return $coll;
    }

    /**
     * 遍历集合中的所有对象，返回包含特定属性值的数组
     *
     * @code php
     * $coll = new QColl('Post');
     * $coll[] = new Post(array('title' => 't1'));
     * $coll[] = new Post(array('title' => 't2'));
     *
     * // 此时 $titles 中包含 t1 和 t2 两个值
     * $titles = $coll->values('title');
     * @endcode
     *
     * @param string $prop_name 要获取集合对象的哪一个属性
     *
     * @return array 包含所有集合对象指定属性值的数组
     */
    function values($prop_name)
    {
        $return = array();
        foreach (array_keys($this->_coll) as $offset)
        {
            if (isset($this->_coll[$offset]->{$prop_name}))
            {
                $return[] = $this->_coll[$offset]->{$prop_name};
            }
        }
        return $return;
    }

    /**
     * 检查指定索引的对象是否存在，实现 ArrayAccess 接口
     *
     * @code php
     * echo isset($coll[1]);
     * @endcode
     *
     * @param mixed $offset
     *
     * @return boolean
     */
    function offsetExists($offset)
    {
        return isset($this->_coll[$offset]);
    }

    /**
     * 返回指定索引的对象，实现 ArrayAccess 接口
     *
     * @code php
     * $item = $coll[1];
     * @endcode
     *
     * @param mixed $offset
     *
     * @return mixed
     */
    function offsetGet($offset)
    {
        if (isset($this->_coll[$offset]))
        {
            return $this->_coll[$offset];
        }
        // LC_MSG: 无效的键名 "%s".
        throw new QException(__('无效的键名 "%s".', $offset));
    }

    /**
     * 设置指定索引的对象，实现 ArrayAccess 接口
     *
     * @code php
     * $coll[1] = $item;
     * @endcode
     *
     * @param mixed $offset
     * @param mixed $value
     */
    function offsetSet($offset, $value)
    {
        if (is_null($offset))
        {
            $offset = count($this->_coll);
        }
        $this->_checkType($value);
        while (isset($this->_coll[$offset])) $offset++;
        $this->_coll[$offset] = $value;
    }

    /**
     * 注销指定索引的对象，实现 ArrayAccess 接口
     *
     * @code php
     * unset($coll[1]);
     * @endcode
     *
     * @param mixed $offset
     */
    function offsetUnset($offset)
    {
        unset($this->_coll[$offset]);
    }

    /**
     * 返回当前位置的对象，实现 Iterator 接口
     *
     * @return mixed
     */
    function current()
    {
        return current($this->_coll);
    }

    /**
     * 返回遍历时的当前索引，实现 Iterator 接口
     *
     * @return mixed
     */
    function key()
    {
        return key($this->_coll);
    }

    /**
     * 遍历下一个对象，实现 Iterator 接口
     */
    function next()
    {
        $this->_is_valid = (false !== next($this->_coll));
    }

    /**
     * 重置遍历索引，实现 Iterator 接口
     */
    function rewind()
    {
        $this->_is_valid = (false !== reset($this->_coll));
    }

    /**
     * 判断是否是调用了 rewind() 或 next() 之后获得的有效对象，实现 Iterator 接口
     *
     * @return boolean
     */
    function valid()
    {
        return $this->_is_valid;
    }

    /**
     * 返回对象总数，实现 Countable 接口
     *
     * @return int
     */
    function count()
    {
        return count($this->_coll);
    }

    /**
     * 确定集合是否是空的
     *
     * @return boolean
     */
    function isEmpty()
    {
        return empty($this->_coll);
    }

    /**
     * 返回集合中的第一个对象，如果没有任何对象，则抛出异常
     *
     * @return object
     */
    function first()
    {
        if (count($this->_coll))
        {
            return reset($this->_coll);
        }
        // LC_MSG: "%s" 集合中没有任何对象.
        throw new QException(__('"%s" 集合中没有任何对象.', $this->_type));
    }

    /**
     * 返回集合中的最后一个对象，如果没有任何对象，则抛出异常
     *
     * @return object
     */
    function last()
    {
        if (count($this->_coll))
        {
            $keys = array_keys($this->_coll);
            $key = array_pop($keys);
            return $this->_coll[$key];
        }
        // LC_MSG: "%s" 集合中没有任何对象.
        throw new QException(__('"%s" 集合中没有任何对象.', $this->_type));
    }

    /**
     * 追加数组或 QColl 对象的内容到集合中
     *
     * @code php
     * $data = array(
     *     $item1,
     *     $item2,
     *     $item3
     * );
     *
     * $coll->append($data);
     * @endcode
     *
     * QColl::append() 在追加数据时不会保持键名。
     *
     * @param array|QColl $data 要追加的数据
     *
     * @return QColl 返回集合对象本身，实现连贯接口
     */
    function append($data)
    {
        if (is_array($data) || ($data instanceof Iterator))
        {
            foreach ($data as $item)
            {
                $this->offsetSet(null, $item);
            }
        }
        else
        {
            // LC_MSG: "%s()" 的参数 "%s" 必须是 "%s"，但实际提供的是 "%s".
            throw new QException(__('"%s()" 的参数 "%s" 必须是 "%s"，但实际提供的是 "%s".',
                    __METHOD__, '$data', '数组或实现了 Iterator 的对象', gettype($data)));
        }

        return $this;
    }

    /**
     * 查找符合指定属性值的对象，没找到返回 NULL
     *
     * @code php
     * // 在 $coll 集合中搜索 title 属性等于 T1 的第一个对象
     * $item = $coll->search('title', 'T1');
     * @endcode
     *
     * @param string $prop_name 要搜索的属性名
     * @param mixed $needle 需要的属性值
     * @param boolean $strict 是否严格比对属性值
     *
     * @return mixed
     */
    function search($prop_name, $needle, $strict = false)
    {
        foreach ($this->_coll as $item)
        {
            if ($strict)
            {
                if ($item->{$prop_name} === $needle) return $item;
            }
            else
            {
                if ($item->{$prop_name} == $needle) return $item;
            }
        }
        return null;
    }

    /**
     * 将集合所有元素的值转换为一个名值对数组
     *
     * @param string $key_name
     * @param string $value_name
     *
     * @return array
     */
    function toHashMap($key_name, $value_name = null)
    {
        $ret = array();
        if ($value_name)
        {
            foreach ($this->_coll as $obj)
            {
                $ret[$obj[$key_name]] = $obj[$value_name];
            }
        }
        else
        {
            foreach ($this->_coll as $obj)
            {
                $ret[$obj[$key_name]] = $obj;
            }
        }

        return $ret;
    }

    /**
     * 对集合中每一个对象调用指定的方法
     *
     * @code php
     * class OrderItem
     * {
     *     public $price;
     *     public $quantity;
     *
     *     function __construct($price, $quantity)
     *     {
     *         $this->price = $price;
     *         $this->quantity = $quantity;
     *     }
     *
     *     // 计算订单项目的小计
     *     function sum()
     *     {
     *         return $this->price * $this->quantity;
     *     }
     *
     *     // 返回单价
     *     function price()
     *     {
     *         return $this->price;
     *     }
     *
     *     // 返回数量
     *     function quantity()
     *     {
     *         return $this->quantity;
     *     }
     *
     *     // 累加多个合计
     *     static function totalSum($objects)
     *     {
     *         $total = 0;
     *         while (list(, $item) = each($objects))
     *         {
     *             $total += $item->sum();
     *         }
     *         return $total;
     *     }
     *
     *     // 用于 QColl 的回调方法
     *     static function _qcoll_callback()
     *     {
     *         return array('sum' => 'totalSum');
     *     }
     * }
     *
     * // 构造一个集合，包含多个 OrderItem 对象
     * $coll = QColl::create(array(
     *     new OrderItem(100, 3),
     *     new OrderItem(200, 5),
     *     new OrderItem(300, 2)), 'OrderItem');
     *
     * // 取得集合中所有订单项目的金额合计
     * $sum = $coll->sum();
     *
     * // 将会输出 1900 （根据 100 * 3 + 200 * 5 + 300 * 2 计算）
     * echo $sum;
     *
     * // 取得每个项目的单价
     * $price = $coll->price();
     * // 将会输出 array(100, 200, 300)
     * dump($price);
     * @endcode
     *
     * 当调用 QColl 自身没有定义的方法时，QColl 将认为开发者是要对集合中的每一个对象调用指定方法。
     *
     * -  此时，QColl 首先检查集合中的对象是否提供了 _qcoll_callback() 静态方法；
     * -  如果有，则通过 _qcoll_callback() 取得一个方法映射表；
     * -  QColl 根据 _qcoll_callback() 返回的方法映射表调用对象的其他静态方法。
     * -  如果没有提供 _qcoll_callback() 方法，或方法映射表中没有指定的方法。
     *    QColl 则遍历集合中的所有对象，尝试调用对象的指定方法。
     *
     * @param string $method
     * @param array $args;
     *
     * @return mixed
     */
    function __call($method, $args)
    {
        $not_implement = false;
        $method = strtolower($method);
        if (method_exists($this->_type, '_qcoll_callback'))
        {
            $map = call_user_func(array($this->_type, '_qcoll_callback'));
            $map = array_change_key_case($map, CASE_LOWER);
            if (isset($map[$method]))
            {
                array_unshift($args, $this->_coll);
                return call_user_func_array(array($this->_type, $map[$method]), $args);
            }
        }

        $result = array();
        foreach ($this->_coll as $object)
        {
            $result[] = call_user_func_array(array($object, $method), $args);
        }

        return $result;
    }

    /**
     * 检查值是否符合类型要求
     *
     * @param object $object
     */
    protected function _checkType($object)
    {
        if (is_object($object))
        {
            if ($object instanceof $this->_type) return;
            $type = get_class($object);
        }
        else
        {
            $type = gettype($object);
        }
        // LC_MSG: 集合只能容纳 "%s" 类型的对象，而不是 "%s" 类型的值.
        throw new QException(__('集合只能容纳 "%s" 类型的对象，而不是 "%s" 类型的值.', $this->_type, $type));
    }

}

